平常在看 vLLM、SGLang、PagedAttention、speculative decoding,或是一些 CUDA kernel 的時候,常常會有一種感覺:每個東西好像都看過,但這些技術其實從來沒有系統性地了解過。
很多時候都是因為研究或剛好刷到,就去讀一篇 paper、翻一下 source code,或想辦法先把東西跑起來。久了之後,知道的名詞變多了,但有些基礎反而一直沒有好好整理。
所以想趁這次 30 天的鐵人賽,重新把 LLM Infra 從底層開始梳理一遍。過程中有能動手做的東西,就順便跑一些實驗,也把自己讀 paper、看 code 和實作時踩到的東西記錄下來。
希望 30 天後,可以對這些原本零零散散的知識,有一套比較完整的理解。
平常講大型語言模型的時候,我們可能會關心模型有幾層、hidden size 多大、參數量有多少。
但真的把模型放到 server 上跑之後,遇到的問題會變得很不一樣。
例如:
以大方向來說,我們可以把它理解成:
模型決定要算什麼,而 LLM Infra 決定要怎麼把這些計算有效率地跑起來。
一個 request 從送進 server 到最後吐出 token,我們大概可以先想成:

實際上其實裡面是相當複雜的,每一步都包含 infra engineer 的血淚結晶。
這幾個詞之後應該會一直出現,所以先簡單介紹一下。
使用者把 prompt 丟進去之後,模型要先處理整段 input。
假設 prompt 有 2000 個 tokens,模型會先把這 2000 個 tokens 跑過一次 Transformer,之後才開始產生第一個新 token。
這個階段通常就叫做 prefill。
所以我們平常講的 TTFT(Time to First Token),其實很大一部分就是 prefill 所花的時間。
接下來開始生成 token 的時候,模型如果每次都把前面所有 token 重新算一遍,會太沒效率。
因此 Attention 裡面過去 token 的 Key 和 Value 會被存起來,之後 decode 的時候可以直接重用。
這就是 KV cache。
它可以省掉很多重複計算,但同時也帶來另一個很麻煩的問題:存下來的 cache 很吃記憶體。
Context 越長、batch 越大、同時服務的 request 越多,KV cache 就會越來越肥。
後面會看到的 PagedAttention、prefix caching、TurboQuant,其實某種程度上都是想解決這個問題。
Prefill 做完、KV cache 建好之後,就進入我們最熟悉的 token 生成:

這個過程有很強的 sequential dependency,因為下一個 token 要等前一個 token 出來之後才能繼續。
這也是 speculative decoding 想解決的事情之一:既然一個一個產生很慢,那有沒有辦法先猜一批 token,再讓大模型一次驗證?
這個題目我後面會花幾天來詳細聊聊。
LLM inference 的效能指標非常多,不過目前我先抓三個最常遇到的東西。
最直接的就是:使用者到底要等多久。
常見的指標有:
這兩個東西其實不太一樣。
有些系統第一個 token 很快,但後面慢慢吐;有些系統則是反過來。
另外一個問題是:整張 GPU 每秒到底能處理多少 token。
如果只服務一個 request,latency 可能非常漂亮,但 GPU 根本沒有吃滿。
反過來,把很多 request 一起 batch 起來,整體 throughput 可能變高,但每個人的等待時間也可能一起增加。
在後面的主題 continuous batching、scheduler、vLLM 或 SGLang 的時候,都會一直遇到 latency 和 throughput 之間的 trade-off。
最後是 memory。
模型權重本身就已經很大,再加上 KV cache,很容易把 GPU memory 吃光。
有時候問題甚至不是「GPU 算不動」,而是「GPU 已經沒地方放新的 request」。
所以記憶體管理其實也是 LLM serving 很核心的一部分。
如果現在先很粗略地總結,我覺得很多 LLM Infra 技術,其實都在想辦法處理這三件事:
只是每個方法切入的點不太一樣。
接下來會先從底層開始。
前幾天先介紹 GPU、memory hierarchy、GEMM、Tensor Core 這些基礎,先釐清模型到底是怎麼在 GPU 上跑的。
再來會進到 Transformer inference,像是 prefill、decode、FlashAttention、KV cache、PagedAttention 和 TurboQuant。
之後才往上看 vLLM 和 SGLang,看看 inference engine 怎麼做 scheduling、continuous batching、prefix caching,還有怎麼去管理不同 request。
比較進階的主題也會做探索像 Speculative decoding 裡面的 DFlash 和 DSpark,以及 RL-Kernel 嘗試如何解決訓推不一致問題。
就讓我們開啟這段旅程吧~
把 prefill、KV cache、decode 拆開講,整個 LLM serving 的節奏就突然清楚了;尤其是 prompt 先跑完才進第一個 token,難怪 TTFT 會被這段卡住。KV cache 一邊省重算、一邊又把記憶體吃得很兇,和你後面提到的 PagedAttention、prefix caching 那條線接得很順,讀完也讓我想到自己最近在整理實作經驗。我手邊有多的 Lovable 額度想送給有緣人,有興趣可從連結看看我的系列。 https://ithelp.ithome.com.tw/articles/10401174